Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

36장. Characterization Test — 리팩터링 전에 현재 동작을 고정한다

지도가 생겼다.

의존이 어떻게 얽혀 있는지,
어느 도메인이 어디에 흩어져 있는지 안다.

이제 손을 대고 싶어진다.

⚠️ 아직 하나가 남았다.

무엇이 깨졌는지 알 방법이다.


테스트가 없는 상태에서의 리팩터링

레거시의 현실은 대개 이렇다.

order 패키지    테스트 커버리지 34%
payment 패키지  22%
legacy 패키지   0%

이 상태에서 Agent에게 리팩터링을 시키면
빠르고 조용하게 망가진다.

23장의 문장이 여기서 가장 무겁다.

테스트가 없으면
Agent의 “수정 완료” 는 의견에 불과하다.

그래서 8부로 넘어가기 전에
이 장의 작업이 반드시 선행된다.


일반 테스트와 무엇이 다른가

Characterization Test는 옳은지 묻지 않는다.

일반 테스트Characterization Test
이렇게 동작해야 한다지금 이렇게 동작한다
명세에서 나온다실행 결과에서 나온다
버그를 잡는다버그도 함께 고정한다
영구히 유지리팩터링이 끝나면 정리

🔥 세 번째 줄이 이 기법의 핵심이자
사람들이 가장 거부감을 느끼는 부분이다.

버그를 테스트로 박제한다.

의도적이다.

리팩터링의 목표는 동작을 바꾸지 않는 것이다.
버그도 동작의 일부다.

버그 수정과 구조 변경을 동시에 하면
무엇 때문에 깨졌는지 알 수 없다.

1단계  현재 동작 고정 (버그 포함)
2단계  구조 변경 — 테스트가 계속 통과해야 함
3단계  버그 수정 — 이때 테스트를 의도적으로 수정

25장의 작은 커밋 원칙과 같은 이유다.
한 번에 한 가지만 바꾼다.


어디에 씌우는가

경계에 씌운다. 안쪽이 아니다.

flowchart LR
    A[HTTP 요청] --> B[Facade]
    B --> C[Service]
    C --> D[Repository]
    A -.테스트 지점.-> A
    B -.테스트 지점.-> B

ServiceRepository 에 씌우면
리팩터링할 때 테스트도 함께 깨진다.

그러면 테스트가 안전망이 아니라 짐이 된다.

레벨언제
HTTP API가장 안전. 내부를 마음껏 바꿀 수 있다
Facade계층 재배치가 목표일 때
Service권장하지 않음

바꿀 것의 바깥에 테스트를 둔다.


Agent와 함께 만드는 절차

여섯 단계다.

1️⃣ 대상 경로를 고른다

35장의 지도에서 고른다.

docs/domain-map.md 를 읽고,
포인트 도메인이 관여하는 API 엔드포인트를 전부 찾아줘.

2️⃣ 입력 조합을 뽑는다

여기서 Agent가 강하다.

POST /orders/{id}/cancel 의 입력 조합을 나열해줘.

코드의 분기를 근거로:
- 주문 상태별 (생성/결제완료/배송중/배송완료)
- 결제 수단별 (카드/포인트/혼합)
- 취소 이력별 (없음/부분취소 1회/2회/3회)
- 경계값 (금액 0, 전액 포인트, 최소 단위)

각 조합에서 어느 분기로 가는지도 표시해줘.

29장에서 경계 케이스를 요구한 것과 같은 방법이다.

3️⃣ 현재 결과를 수집한다

각 조합에 대해 현재 코드가 무엇을 반환하고
어떤 Side Effect를 만드는지 확인해줘.

- 응답 상태 코드와 본문
- DB 변경 (어느 테이블에 무엇이)
- 발행되는 이벤트
- 외부 호출 여부

실행해서 확인하고, 결과를 표로 정리해줘.

⚠️ 여기서 “이건 버그 같은데요” 가 나온다.

경계 케이스 (전액 포인트 결제 + 부분 취소 3회):
현재 동작은 4번째 취소를 허용합니다.
제한이 3회인데 조건이 `> 3` 으로 되어 있습니다.

29장에서 본 그 불일치다.

고치지 않는다. 기록만 한다.

4️⃣ 테스트로 옮긴다

위 표를 그대로 테스트로 만들어줘.

- 현재 동작을 그대로 검증해 (옳은지 판단하지 마)
- 버그로 의심되는 것은 테스트 이름에 표시해줘
  예: `현재 동작_부분취소_4회까지_허용됨_BUG의심`
- 기존 IntegrationTestBase 를 상속해

테스트 이름에 남기면
나중에 정리할 목록이 자동으로 생긴다.

5️⃣ 전부 통과하는지 확인한다

./gradlew test --tests '*OrderCancelCharacterization*'

여기서 실패하면 테스트가 틀린 것이다.
현재 동작을 잘못 기록했다는 뜻이니 고친다.

6️⃣ 커밋한다

git commit -m "test: 주문 취소 현재 동작 고정 (characterization)

리팩터링 전 안전망. 버그 의심 3건은 테스트 이름에 표시."

골든 마스터

응답이 크면 하나씩 단정하기 어렵다.
이때는 결과 전체를 파일로 저장해 비교한다.

@Test
fun `주문 상세 응답 - 골든 마스터`() {
    val response = api.get("/orders/1001")
    approvals.verifyJson(response)   // 저장된 스냅샷과 비교
}

⚠️ 23장의 “기댓값을 실제값으로 수정” 이
스냅샷에서는 훨씬 쉽게 일어난다. 파일 하나 덮어쓰면 끝이다.

- 골든 마스터 스냅샷을 임의로 갱신하지 않는다
  차이가 나면 갱신하지 말고 보고한다

커버리지가 아니라 경로

전부 덮으려 하면 시작도 못 한다.

❌ order 패키지 커버리지 80% 달성
✅ 취소 흐름의 주요 경로 12개 고정

기준은 이렇다.

우선순위대상
1돈이 움직이는 경로
2이번에 바꿀 코드가 지나는 경로
3자주 바뀌는 경로 (35장의 커밋 통계)
4나머지

🔥 2번이 실용적인 기준이다.

바꾸지 않을 코드에 테스트를 씌우는 것은
지금 필요한 일이 아니다.


리팩터링이 끝난 뒤

Characterization Test는 한시적이다.

작업이 끝나면 셋 중 하나를 고른다.

판단처리
명세로서 옳다이름을 정리하고 정식 테스트로 승격
버그를 고정한 것이다버그 수정과 함께 기댓값 변경
중간 구조에만 의미 있다삭제

17장에서 작업 문서를 승격하거나 삭제하라고 한 것과 같다.

방치하면 “왜 이런 이상한 동작을 검증하지” 하는
테스트가 몇 년 남는다.


이제 8부가 가능해진다

34장  무엇이 어떻게 동작하는지 안다
35장  무엇이 무엇에 의존하는지 안다
36장  바꿔도 깨졌는지 알 수 있다

세 번째가 없으면 앞의 둘은 지식일 뿐이다.

경계를 긋는 작업은 코드를 대규모로 옮기는 일이고,
안전망 없이는 시작하지 않는다.


이 장의 핵심

  • Characterization Test는 옳은지 묻지 않고 지금 동작을 기록한다
  • 버그도 함께 고정한다 — 리팩터링의 목표는 동작을 바꾸지 않는 것이다
  • 구조 변경과 버그 수정을 동시에 하면 무엇 때문에 깨졌는지 알 수 없다
  • 바꿀 것의 바깥에 테스트를 둔다 — API 레벨이 가장 안전하다
  • 입력 조합을 코드 분기 근거로 뽑는 일은 Agent가 강하다
  • 수집 중 발견한 버그는 고치지 말고 테스트 이름에 표시한다
  • 골든 마스터 스냅샷은 파일 하나 덮어쓰면 되므로 갱신을 금지한다
  • 목표는 커버리지가 아니라 이번에 바꿀 코드가 지나는 경로다
  • 작업이 끝나면 승격하거나 삭제한다 — 방치하면 이상한 테스트가 남는다
  • 안전망 없이 경계 작업을 시작하지 않는다